package com.lipaluma.field.mapper.single.impl;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.lipaluma.context.Context;
import com.lipaluma.exceptions.CsvParserException;
import com.lipaluma.field.mapper.AbstractFieldMapper;
import com.lipaluma.field.mapper.FieldId;
import com.lipaluma.field.mapper.FieldMapper;
import com.lipaluma.field.mapper.initializer.FieldMapperInitializer;
import com.lipaluma.field.mapper.initializer.XmlFieldMapperInitializer;
import com.lipaluma.field.mapper.mapping.FieldMappingInfo;
import com.lipaluma.field.mapper.mapping.TargetMappingInfo;
import com.lipaluma.field.merge.MergingTargetFunction;
import com.lipaluma.io.Row;
import com.lipaluma.parser.AbstractDataParser;
import com.lipaluma.parser.marshalling.ObjectMarshalled;
import com.lipaluma.xml.mapping.XmlFieldMapping;
import com.lipaluma.xml.mapping.XmlObjectFieldMapping;

/**
 * This Class is the MapColumn instantiated by default when the type is not recognized.
 * <p>
 * The object column will map the unrecognized type with a new Mapper for that type.<br/>
 * This object type can be used like the target POJO and map columns of the entry file.<br/>
 * This column allows user to have per example a target class like that :<br/>
 * 
 * <code>
 * <br/>
 * Class Customer {<br/>
 * &nbsp;&nbsp;&nbsp;@MapColumn(index=1)<br/>
 * &nbsp;&nbsp;&nbsp;private String name;<br/>
 * &nbsp;&nbsp;&nbsp;@MapColumn(index=2)<br/>
 * &nbsp;&nbsp;&nbsp;private String firstname;<br/>
 * <br/>
 * &nbsp;&nbsp;&nbsp;private Address address;<br/>
 * }<br/>
 * <br/>
 * Class Address {<br/>
 * &nbsp;&nbsp;&nbsp;@MapColumn(index=3)<br/>
 * &nbsp;&nbsp;&nbsp;private String street;<br/>
 * &nbsp;&nbsp;&nbsp;@MapColumn(index=4)<br/>
 * &nbsp;&nbsp;&nbsp;private String zip;<br/>
 * &nbsp;&nbsp;&nbsp;@MapColumn(index=5)<br/>
 * &nbsp;&nbsp;&nbsp;private String city;<br/>
 * }
 * </code>
 * 
 * @author Mario
 */
public class ObjectFieldMapper extends AbstractFieldMapper implements FieldMapperInitializer, XmlFieldMapperInitializer {

	private RowDataParser<?> parser;
	private List<FieldMapper> fieldMappersDefined = new ArrayList<FieldMapper>();
	private Class<?> targetClass;
	private MergingTargetFunction mergingTargetFunction;
	private boolean targetCanBeMerged;
	private TargetMappingInfo targetMappingInfo;
	
	public ObjectFieldMapper() {}
	/**
	 * create a column that parses an object of the user.
	 * @param parser the parser that will map the user object
	 */
	public ObjectFieldMapper(List<FieldMapper> fieldMappers) {
		if(fieldMappers == null || fieldMappers.isEmpty())
			throw new CsvParserException("Error on creating ObjectFieldMapper : fieldMappers can't be null or empty");
		this.fieldMappersDefined = fieldMappers;
		this.targetClass = fieldMappers.get(0).getField().getDeclaringClass();		
	}
	public ObjectFieldMapper(List<FieldMapper> fieldMappers, Class<?> targetClass) {
		if(fieldMappers == null || fieldMappers.isEmpty())
			throw new CsvParserException("Error on creating ObjectFieldMapper : fieldMappers can't be null or empty");
		this.fieldMappersDefined = fieldMappers;
		this.targetClass = targetClass;
	}
	public ObjectFieldMapper(List<FieldMapper> fieldMappers, boolean targetCanBeMerged) {
		if(fieldMappers == null || fieldMappers.isEmpty())
			throw new CsvParserException("Error on creating ObjectFieldMapper : fieldMappers can't be null or empty");
		this.fieldMappersDefined = fieldMappers;
		this.targetClass = fieldMappers.get(0).getField().getDeclaringClass();		
		this.targetCanBeMerged = targetCanBeMerged;
	}
	public ObjectFieldMapper(List<FieldMapper> fieldMappers, Class<?> targetClass, boolean targetCanBeMerged) {
		if(fieldMappers == null || fieldMappers.isEmpty())
			throw new CsvParserException("Error on creating ObjectFieldMapper : fieldMappers can't be null or empty");
		this.fieldMappersDefined = fieldMappers;
		this.targetClass = targetClass;
		this.targetCanBeMerged = targetCanBeMerged;
	}
	public ObjectFieldMapper(List<FieldMapper> fieldMappers, MergingTargetFunction mergingTargetFunction) {
		if(fieldMappers == null || fieldMappers.isEmpty())
			throw new CsvParserException("Error on creating ObjectFieldMapper : fieldMappers can't be null or empty");
		this.fieldMappersDefined = fieldMappers;
		this.targetClass = fieldMappers.get(0).getField().getDeclaringClass();		
		this.mergingTargetFunction = mergingTargetFunction;
		this.targetCanBeMerged = true;
	}
	public ObjectFieldMapper(List<FieldMapper> fieldMappers, Class<?> targetClass, MergingTargetFunction mergingTargetFunction) {
		if(fieldMappers == null || fieldMappers.isEmpty())
			throw new CsvParserException("Error on creating ObjectFieldMapper : fieldMappers can't be null or empty");
		this.fieldMappersDefined = fieldMappers;
		this.targetClass = targetClass;
		this.mergingTargetFunction = mergingTargetFunction;
		this.targetCanBeMerged = true;
	}
	
	@Override
	public void init(Context context, Field field) {
		this.field = field;
		if(targetClass == null) {
			targetClass = context.getBasicMappingManager().getTargetClass(field);
		}
		parser = new RowDataParser(targetClass, context, fieldMappersDefined);
		if(context.isMappedByAnnotation()) {
			this.targetMappingInfo = context.getMappingManagerByAnnotation().getTargetMappingInfo(targetClass);
		}
	}

	@Override
	public void init(Context context, Field field, XmlFieldMapping mapping) {
		this.field = field;
		if(!XmlObjectFieldMapping.class.isAssignableFrom(mapping.getClass()))
			throw new CsvParserException("Error on xml configuration to map field "+field.getName()+". To map an complexe object, use map-object node");
		XmlObjectFieldMapping objectMapping = (XmlObjectFieldMapping)mapping;
		targetClass = context.getMappingManagerByXml().getTargetClass(field, objectMapping);
		parser = new RowDataParser(targetClass, context, objectMapping);
//		if(targetCanBeMerged)
//			parser.targetCanBeMerged();
	}
	
	@Override
	public Object unmarshal(Row row) {
		return parser.unmarshal(row);
	}

	@Override
	public void marshalValue(Object value, ObjectMarshalled marshal) {
		marshal.copy(parser.marshal(value));
	}

	public String getDescription() {
		return "[MapColumn "+this.getClass()+" (field:"+field.getName()+")]\n";
	}
	
	@Override
	public String generateKey(Object target) {
		List<FieldMapper> fieldMappers = parser.getFieldMappers();
		StringBuilder key = new StringBuilder();
		for (FieldMapper fieldMapper : fieldMappers) {
			if(FieldId.class.isAssignableFrom(fieldMapper.getClass()))
				key.append(((FieldId)fieldMapper).generateKey(target));
		}
		return key.toString();
	}

	@Override
	public FieldMappingInfo[] getMappings() {
		List<FieldMappingInfo> result = new ArrayList<FieldMappingInfo>();
		for(FieldMapper mapper : parser.getFieldMappers()) {
			result.addAll(Arrays.asList(mapper.getMappings()));
		}
		return result.toArray(new FieldMappingInfo[0]);
	}

	public void setTargetMappingInfo(TargetMappingInfo targetMappingInfo) {
		this.targetMappingInfo = targetMappingInfo;
	}
	
	public TargetMappingInfo getTargetMappingInfo() {
		return targetMappingInfo;
	}
	
	public List<FieldMapper> getFieldMappers() {
		return parser.getFieldMappers();
	}

	public Class<?> getTargetClass() {
		return targetClass;
	}

	public void setTargetCanBeMerged(boolean targetCanBeMerged){
		this.targetCanBeMerged = targetCanBeMerged;
	}
	
	public boolean isTargetCanBeMerged() {
		return targetCanBeMerged ;
	}
	
	public void setMergingTargetFunction(MergingTargetFunction mergingTargetFunction) {
		this.targetCanBeMerged = true;
		this.mergingTargetFunction = mergingTargetFunction;
	}
	
	public MergingTargetFunction getMergingTargetInfos() {
		return mergingTargetFunction;
	}
	
	static class RowDataParser<TARGET> extends AbstractDataParser<TARGET> {
		public RowDataParser(Class<TARGET> targetClass, Context context, List<FieldMapper> fieldMappers) {
			this.targetClass = targetClass;
			this.context = context;
			if(fieldMappers == null || fieldMappers.isEmpty())
				this.fieldMappers = super.declareFields(context, targetClass);
			else
				this.fieldMappers = fieldMappers;
		}
		
		public RowDataParser(Class<TARGET> targetClass, Context context, XmlObjectFieldMapping mapping) {
			this.targetClass = targetClass;
			this.context = context;
			this.fieldMappers = declareFields(context, targetClass, mapping.getMappings());
		}

		@Override
		protected void initParser() {}
		
		@Override
		protected ObjectMarshalled marshal(Object object) {
			return super.marshal(object);
		}
		@Override
		protected TARGET unmarshal(Row row) {
			return super.unmarshal(row);
		}

		private List<FieldMapper> getFieldMappers() {
			return fieldMappers;
		}
		
		protected Class<?> getTargetClass(){
			return targetClass;
		}
		
		@Override
		protected Context createContext() {
			return null;
		}
	}

}
